﻿namespace MvvmTools.NotifyPropertyChanged
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Diagnostics;
    using System.Diagnostics.Contracts;
    using System.Linq;
    using System.Threading;
    using JetBrains.Annotations;

    /// <summary>
    /// Base class for change listener.
    /// </summary>
    internal abstract class ChangeListenerBase : IPropertyObservable
    {
        private readonly List<ChangeListenerBase> _children = new List<ChangeListenerBase>();
        private readonly Lazy<HashSet<IObserver<PropertyChange>>> _observers = new Lazy<HashSet<IObserver<PropertyChange>>>(() => new HashSet<IObserver<PropertyChange>>());
        private readonly PropertyChangedHolder _propertyChangedHolder;
        private readonly string _propertyName;
        private int _changeId;
        private long _suspendCounter;

        /// <summary>
        /// Initializes a new instance of the <see cref="ChangeListenerBase"/> class.
        /// </summary>
        /// <param name="parent">
        /// The parent.
        /// </param>
        /// <param name="getter">
        /// The getter.
        /// </param>
        /// <param name="propertyName">
        /// The property name.
        /// </param>
        protected ChangeListenerBase([CanBeNull] ChangeListenerBase parent, [NotNull] Func<object, INotifyPropertyChanged> getter, [CanBeNull] string propertyName)
        {
            Contract.Requires<ArgumentNullException>(getter != null);

            _propertyName = propertyName;
            Parent = parent;
            Getter = getter;
            _propertyChangedHolder = new PropertyChangedHolder(this);

            if (parent == null)
            {
                return;
            }

            parent.AddChild(this);
            var parentObject = Parent.GetObject();
            if (parentObject != null)
            {
                _propertyChangedHolder.Subscribe(parentObject);
            }
        }

        /// <summary>
        /// Gets target object for subscription.
        /// </summary>
        public INotifyPropertyChanged Target
        {
            get
            {
                Contract.Ensures(Contract.Result<INotifyPropertyChanged>() != null);

                return _propertyChangedHolder.Target;
            }
        }

        /// <summary>
        /// Gets the rule of getting INotifyPropertyChanged object.
        /// </summary>
        protected Func<object, INotifyPropertyChanged> Getter { get; private set; }
        
        /// <summary>
        /// Gets a value indicating whether this instance is suspended.
        /// </summary>
        /// <value>
        /// <c>true</c> if this instance is suspended; otherwise, <c>false</c>.
        /// </value>
        protected bool IsSuspended
        {
            [DebuggerStepThrough]
            get { return _suspendCounter != 0; }
        }
        
        /// <summary>
        /// Gets the property name that is under observation.
        /// </summary>
        protected string PropertyName
        {
            [DebuggerStepThrough]
            get { return _propertyName; }
        }

        // Default initialization for a bool is 'false' 
        private bool IsDisposed { get; set; }

        private ChangeListenerBase Parent { [CanBeNull] get; [CanBeNull] set; }

        /// <summary> 
        /// Implementation of Dispose according to .NET Framework Design Guidelines. 
        /// </summary> 
        /// <remarks>Do not make this method virtual. 
        /// A derived class should not be able to override this method. 
        /// </remarks> 
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        public IDisposable Subscribe([NotNull] IObserver<PropertyChange> observer)
        {
            Contract.Requires<ArgumentNullException>(observer != null);
            Contract.Ensures(Contract.Result<IDisposable>() != null);

            lock (_observers)
            {
                _observers.Value.Add(observer);
            }

            return new Subscriber(this, observer);
        }

        /// <summary>
        /// Suspend any notifications.
        /// </summary>
        /// <returns>
        /// Disposable object, which must be removed for resuming notifications.
        /// </returns>
        public IDisposable Suspend()
        {
            Contract.Ensures(Contract.Result<IDisposable>() != null);

            return new Suspender(this);
        }

        /// <summary>
        /// Overloaded Implementation of Dispose. 
        /// </summary>
        /// <param name="disposing">
        /// <c>True</c> if disposing is on the way.
        /// </param>
        /// <remarks>
        /// <para>
        /// <list type="bulleted">
        /// Dispose executes in two distinct scenarios. 
        /// <item>
        /// If <paramref name="disposing"/> equals true, the method has been called directly 
        /// or indirectly by a user's code. Managed and unmanaged resources 
        /// can be disposed.
        /// </item>
        /// <item>
        /// If <paramref name="disposing"/> equals false, the method has been called by the  
        /// runtime from inside the collector and you should not reference  
        /// other objects. Only unmanaged resources can be disposed.
        /// </item>
        /// </list>
        /// </para>
        /// </remarks>
        protected virtual void Dispose(bool disposing)
        {
            if (IsDisposed)
            {
                return;
            }

            try
            {
                if (disposing)
                {
                    if (_children.Count != 0)
                    {
                        throw new InvalidOperationException("Instance cannot be disposed");
                    }
                    
                    GetRoot().Unsubscribe(true);
                    lock (_observers)
                    {
                        foreach (var observer in _observers.Value)
                        {
                            observer.OnCompleted();
                        }

                        _observers.Value.Clear();
                    }
                }
            }
            finally
            {
                IsDisposed = true;
            }
        }

        /// <summary>
        /// Gets the instance of INotifyPropertyChanged object.
        /// </summary>
        /// <returns>
        /// Instance of the INotifyPropertyChanged.
        /// </returns>
        protected virtual INotifyPropertyChanged GetObject()
        {
            var parentObj = Parent.GetObject();
            return parentObj == null ? null : Getter(parentObj);
        }

        /// <summary>
        /// Raise changes in root object.
        /// </summary>
        /// <param name="source">
        /// The source of changes.
        /// </param>
        /// <param name="change">
        /// The information about changed property.
        /// </param>
        protected virtual void RaiseChangedToRoot(ChangeListenerBase source, PropertyChange change)
        {
            Parent.RaiseChangedToRoot(source, change);
        }

        protected void RaiseChangedToRootCompleted(ChangeListenerBase source, PropertyChange change, bool propogateChanges)
        {
            propogateChanges = propogateChanges || source == this;
            foreach (var child in _children)
            {
                child.RaiseChangedToRootCompleted(source, change, propogateChanges);
            }

            if (!propogateChanges)
            {
                return;
            }

            IObserver<PropertyChange>[] currentObservers;
            lock (_observers)
            {
                currentObservers = _observers.Value.ToArray();
            }

            foreach (var observer in currentObservers)
            {
                observer.OnNext(change);
            }
        }

        protected virtual void Subscribe(bool applyToCurrent)
        {
            if (applyToCurrent)
            {
                var parentObject = Parent.GetObject();
                if (parentObject != null)
                {
                    _propertyChangedHolder.Subscribe(parentObject);
                }
            }

            foreach (var child in _children)
            {
                child.Subscribe(true);
            }
        }

        protected virtual void Unsubscribe(bool applyToCurrent)
        {
            foreach (var child in _children)
            {
                child.Unsubscribe(true);
            }

            if (applyToCurrent)
            {
                _propertyChangedHolder.Unsubscribe();
            }
        }

        private void AddChild([NotNull] ChangeListenerBase newChild)
        {
            Contract.Requires<ArgumentNullException>(newChild != null);            
            _children.Add(newChild);
        }

        private void AddSuspender()
        {
            Interlocked.Increment(ref _suspendCounter);
            if (Parent != null)
            {
                Parent.AddSuspender();
            }
        }

        private ChangeListenerBase GetRoot()
        {
            return Parent == null ? this : Parent.GetRoot();
        }

        private void PropertyChanged(string changedPropertyName)
        {
            if (IsSuspended)
            {
                return;
            }

            if (_propertyName == null || changedPropertyName == string.Empty || changedPropertyName == _propertyName)
            {
                Unsubscribe(false);
                Subscribe(false);
                RaiseChangedToRoot(this, new PropertyChange(Interlocked.Increment(ref _changeId), changedPropertyName));
            }
        }

        private void RemoveObserver(IObserver<PropertyChange> observer)
        {
            lock (_observers)
            {
                _observers.Value.Remove(observer);
            }
        }

        private void RemoveSuspender()
        {
            if (Parent != null)
            {
                Parent.RemoveSuspender();
            }

            Interlocked.Decrement(ref _suspendCounter);
        }

        private class PropertyChangedHolder
        {
            private readonly WeakReference _owner;
            private INotifyPropertyChanged _target;

            public PropertyChangedHolder(ChangeListenerBase owner)
            {
                _owner = new WeakReference(owner);
            }

            public INotifyPropertyChanged Target
            {
                get { return _target; }
            }

            public void Subscribe(INotifyPropertyChanged notifyPropertyChanged)
            {
                Unsubscribe();
                _target = notifyPropertyChanged;
                notifyPropertyChanged.PropertyChanged += PropertyChanged;
            }

            public void Unsubscribe()
            {
                var curTarget = _target;
                if (curTarget == null)
                {
                    return;
                }

                curTarget.PropertyChanged -= PropertyChanged;
                _target = null;
            }

            private void PropertyChanged(object sender, PropertyChangedEventArgs e)
            {
                var listener = _owner.Target as ChangeListenerBase;
                if (listener != null)
                {
                    listener.PropertyChanged(e.PropertyName);
                }
                else
                {
                    Unsubscribe();
                }
            }
        }

        private class Subscriber : IDisposable
        {
            private readonly ChangeListenerBase _notificationListener;
            private readonly IObserver<PropertyChange> _observer;

            public Subscriber([NotNull] ChangeListenerBase notificationListener, [NotNull] IObserver<PropertyChange> observer)
            {
                Contract.Requires<ArgumentNullException>(notificationListener != null);
                Contract.Requires<ArgumentNullException>(observer != null);
                _notificationListener = notificationListener;
                _observer = observer;
            }

            public void Dispose()
            {
                _notificationListener.RemoveObserver(_observer);
            }
        }

        private class Suspender : IDisposable
        {
            private readonly ChangeListenerBase _changeListener;

            public Suspender([NotNull] ChangeListenerBase changeListener)
            {
                Contract.Requires<ArgumentNullException>(changeListener != null);
                _changeListener = changeListener;
                changeListener.AddSuspender();
            }

            private bool IsDisposed { get; set; }

            public void Dispose()
            {
                Dispose(true);
                GC.SuppressFinalize(this);
            }

            private void Dispose(bool isDisposing)
            {
                try
                {
                    if (!IsDisposed && isDisposing)
                    {
                        _changeListener.RemoveSuspender();
                    }
                }
                finally
                {
                    IsDisposed = true;
                }
            }
        }
    }
}